package com.umeox.moto.mqtt;

import android.content.Context;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;

import com.umeox.moto.mqtt.base.ARequest;
import com.umeox.moto.mqtt.base.ASend;
import com.umeox.moto.mqtt.base.IOnCallListener;
import com.umeox.moto.mqtt.base.PersistentConnectState;
import com.umeox.moto.mqtt.base.PersistentInitParams;
import com.umeox.moto.mqtt.event.IOnSubscribeListener;
import com.umeox.moto.mqtt.event.IOnSubscribeRpcListener;
import com.umeox.moto.mqtt.event.PersistentEventDispatcher;
import com.umeox.moto.mqtt.mode.HttpsHelp;
import com.umeox.moto.mqtt.mode.MqttTrustManager;
import com.umeox.moto.mqtt.mode.UmeoxMqttAsyncClient;
import com.umeox.moto.mqtt.request.MqttSubscribeRequest;
import com.umeox.moto.mqtt.send.MqttSend;
import com.umeox.moto.mqtt.send.MqttSendExecutor;

import org.eclipse.paho.client.mqttv3.IMqttActionListener;
import org.eclipse.paho.client.mqttv3.IMqttToken;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.Map;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;

public class MqttNet implements IPersisitentNet {

    private Context mContext;
    private MemoryPersistence memoryPersistence;
    private MqttAsyncClient mqttAsyncClient;
    private SSLSocketFactory socketFactory;
    private MqttConnectOptions mqttConnectOptions;
    private InputStream inputStream;
    private boolean isOnline = false;
    private PersistentConnectState connectState;
    private MqttDefaultCallback callback;
    private MqttInitParams mqttParams;

    private MqttNet() {
        changeConnectState(PersistentConnectState.DISCONNECTED);
        callback = null;
    }

    public static MqttNet getInstance() {
        return InstanceHolder.INSTANCE;
    }


    @Override
    public void init(Context context, PersistentInitParams params) throws MqttException {
        if (BuildConfig.DEBUG) {
            Log.d("MqttNet", "init()");
        }
        if (!isOnline
                && connectState != PersistentConnectState.CONNECTING
                && connectState != PersistentConnectState.CONNECTED) {
            if (context != null && params instanceof MqttInitParams && ((MqttInitParams) params).checkValid()) {
                mContext = context;
                mqttParams = (MqttInitParams) params;
                MqttConfigure.mqttClientId = mqttParams.deviceName;
                MqttConfigure.mqttPassword = mqttParams.deviceSecret;
                MqttConfigure.cleanSession = !mqttParams.receiveOfflineMsg;

//                if (MqttConfigure.mqttRootCrtFile == null) {
//                    Log.d("MqttNet", "init(),default cert file");
//                    try {
//                        inputStream = context.getAssets().open("root.crt");
//                    } catch (Exception e) {
//                        Log.e("MqttNet", "setCertFile : cannot config cert file：" + e.getMessage());
//                    }
//                } else {
//                    Log.d("MqttNet", "init(),custom cert file");
//                    inputStream = MqttConfigure.mqttRootCrtFile;
//                }
                connect();
            } else {
                if (BuildConfig.DEBUG) {
                    Log.e("MqttNet", "init error ,params error");
                }
            }
        } else {
            if (BuildConfig.DEBUG) {
                Log.d("MqttNet", "init(), already init, ignore init call!");
            }
        }
    }

    public void reconnectManually() {
        try {
            connect();
        } catch (MqttException e) {
            e.printStackTrace();
        }
    }

    private void connect() throws MqttException {
        String host = MqttConfigure.mqttHost;
        if (TextUtils.isEmpty(MqttConfigure.mqttHost)) {
            host = "ssl://" + MqttConfigure.DEFAULT_HOST;
        }

        String clientId = MqttConfigure.mqttClientId;
        String userName = MqttConfigure.mqttUserName;
        String password = MqttConfigure.mqttPassword;

        if (memoryPersistence != null) {
            memoryPersistence.close();
        }
        memoryPersistence = new MemoryPersistence();

        if (mqttAsyncClient != null) {
            try {
                mqttAsyncClient.close();
            } catch (MqttException e) {
                if (BuildConfig.DEBUG) {
                    Log.e("MqttNet", "mqttAsyncClient close error" + e.getMessage());
                }
                if (e.getReasonCode() == MqttException.REASON_CODE_CONNECT_IN_PROGRESS) {
                    return;
                }
            }
        }
        mqttAsyncClient = new UmeoxMqttAsyncClient(host, clientId, memoryPersistence);

        mqttConnectOptions = new MqttConnectOptions();
        mqttConnectOptions.setMqttVersion(MqttConnectOptions.MQTT_VERSION_3_1_1);
        if (BuildConfig.USE_TLS && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            try {
                HttpsHelp.SSLParams params = HttpsHelp.getTlsSocketFactory();
                socketFactory = params.sSLSocketFactory;
                mqttConnectOptions.setSocketFactory(this.socketFactory);
            } catch (Exception e) {
                if (BuildConfig.DEBUG) {
                    Log.e("MqttNet", "create SSL Socket error" + e.toString());
                    e.printStackTrace();
                }
            }
        }
        mqttConnectOptions.setAutomaticReconnect(true);
        mqttConnectOptions.setCleanSession(MqttConfigure.cleanSession);
        mqttConnectOptions.setUserName(userName);
        mqttConnectOptions.setPassword(password.toCharArray());
        mqttConnectOptions.setConnectionTimeout(10);
        mqttConnectOptions.setKeepAliveInterval(MqttConfigure.getKeepAliveInterval());
        mqttConnectOptions.setMaxInflight(1000);
        mqttConnectOptions.setMaxReconnectDelay(16 * 1000);

        callback = new MqttDefaultCallback();
        mqttAsyncClient.setCallback(callback);
        try {
            changeConnectState(PersistentConnectState.CONNECTING);
            mqttAsyncClient.connect(mqttConnectOptions, null, new IMqttActionListener() {
                @Override
                public void onSuccess(IMqttToken token) {
                    if (BuildConfig.DEBUG) {
                        Log.d("MqttNet", "connect onSuccess");
                    }
                    isOnline = true;
                    changeConnectState(PersistentConnectState.CONNECTED);
                }

                @Override
                public void onFailure(IMqttToken token, Throwable throwable) {
                    changeConnectState(PersistentConnectState.CONNECTFAIL);
                    PersistentEventDispatcher.getInstance().handlerMessage(PersistentEventDispatcher.MSG_CONNECT_STATE_CONNECTFAIL, String.valueOf(token.getException().getReasonCode()));
                    if (BuildConfig.DEBUG) {
                        Log.d("MqttNet", "connect onFailure, cause = " + throwable.toString());
                    }
                }
            });
            if (BuildConfig.DEBUG) {
                Log.d("MqttNet", "mqtt client connect.." + host);
            }
        } catch (Exception e) {
            changeConnectState(PersistentConnectState.CONNECTFAIL);
            PersistentEventDispatcher.getInstance().handlerMessage(PersistentEventDispatcher.MSG_CONNECT_STATE_CONNECTFAIL, e.toString());
            if (BuildConfig.DEBUG) {
                Log.e("MqttNet", " mqtt client connect error,mqttConnectOptions" + e.toString());
                e.printStackTrace();
            }
        }
    }

    public Context getContext() {
        return mContext;
    }

    public MqttAsyncClient getMqttClient() {
        return mqttAsyncClient;
    }

    public PersistentInitParams getInitParams() {
        return mqttParams;
    }

    @Override
    public PersistentConnectState getConnectState() {
        if (BuildConfig.DEBUG) {
            Log.d("MqttNet", "getConnectState()");
        }
        if (getMqttClient() == null) {
            if (BuildConfig.DEBUG) {
                Log.d("MqttNet", "getConnectState() client is empty");
            }
            changeConnectState(PersistentConnectState.DISCONNECTED);
        } else {
            if (BuildConfig.DEBUG) {
                Log.d("MqttNet", "getConnectState() paho state = " + getMqttClient().isConnected());
            }
            changeConnectState(getMqttClient().isConnected() ? PersistentConnectState.CONNECTED : PersistentConnectState.DISCONNECTED);
        }
        return connectState;
    }

    public void changeConnectState(PersistentConnectState state) {
        connectState = state;
    }

    @Override
    public ASend asyncSend(ARequest request, IOnCallListener listener) {
        MqttSend mqttSend = new MqttSend(request, listener);
        MqttSendExecutor.createExecutor().asyncSend(mqttSend);
        return mqttSend;
    }

    @Override
    public void retry(ASend send) {
        MqttSendExecutor.createExecutor().asyncSend(send);
    }

    @Override
    public void subscribe(String topic, IOnSubscribeListener listener) {
        if (TextUtils.isEmpty(topic)) {
            if (BuildConfig.DEBUG) {
                Log.d("MqttNet", "subscribe, topic is empty");
            }
        } else {
            MqttSendExecutor.createExecutor().asyncSend(new MqttSend(MqttSubscribeRequest.createRequest(topic, true), listener));
        }
    }

    @Override
    public void unSubscribe(String topic, IOnSubscribeListener listener) {
        if (TextUtils.isEmpty(topic)) {
            if (BuildConfig.DEBUG) {
                Log.d("MqttNet", "unSubscribe, topic is empty");
            }
        } else {
            MqttSendExecutor.createExecutor().asyncSend(new MqttSend(MqttSubscribeRequest.createRequest(topic, false), listener));
        }
    }

    @Override
    public void subscribeRpc(String topic, final IOnSubscribeRpcListener listener) {
        if (BuildConfig.DEBUG) {
            Log.d("MqttNet", "subscribeRrpc(),topic = " + topic);
        }
        if (!TextUtils.isEmpty(topic) && listener != null) {
            subscribe(topic, listener.getSubscribeListener());
            if (callback != null) {
                if (BuildConfig.DEBUG) {
                    Log.d("MqttNet", "subscribeRrpc(), registerRrpcListener");
                }
                callback.registerRrpcListener(topic, listener);
            }
        } else {
            if (BuildConfig.DEBUG) {
                Log.d("MqttNet", "subscribeRpc(), params error");
            }
        }
    }

    @Override
    public void unSubscribeRpc(String topic, IOnSubscribeRpcListener listener) {
        if (TextUtils.isEmpty(topic) || listener == null) {
            if (BuildConfig.DEBUG) {
                Log.d("MqttNet", "unSubscribe(), params error");
            }
        } else {
            unSubscribe(topic, listener.getSubscribeListener());
            if (callback != null) {
                if (BuildConfig.DEBUG) {
                    Log.d("MqttNet", "subscribeRrpc(), registerRrpcListener");
                }
                callback.unRegisterRrpcListener(topic);
            }
        }
    }

    @Override
    public void destroy() {
        if (BuildConfig.DEBUG) {
            Log.d("MqttNet", "destroy()");
        }
        isOnline = false;
        if (getMqttClient() == null) {
            if (BuildConfig.DEBUG) {
                Log.d("MqttNet", "destroy(), client is null");
            }
        } else {
            try {
                if (mqttAsyncClient != null) {
                    mqttAsyncClient.disconnect();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            if (BuildConfig.DEBUG) {
                Log.d("MqttNet", "destroy(),disconnect");
            }
            changeConnectState(PersistentConnectState.DISCONNECTED);
            PersistentEventDispatcher.getInstance().handlerMessage(PersistentEventDispatcher.MSG_CONNECT_STATE_DISCONNECTED, "disconnect success");
            try {
                if (memoryPersistence != null) {
                    memoryPersistence.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            memoryPersistence = null;
            mqttAsyncClient = null;
            inputStream = null;
            socketFactory = null;
            mqttParams = null;
        }
    }

    @Override
    public void disConnectManually(IMqttActionListener listener) {
        if (BuildConfig.DEBUG) {
            Log.d("MqttNet", "disConnectManually()");
        }
        isOnline = false;
        if (getMqttClient() == null) {
            if (BuildConfig.DEBUG) {
                Log.d("MqttNet", "disConnectManually(), client is null");
            }
        } else {
            try {
                if (mqttAsyncClient != null) {
                    mqttAsyncClient.disconnect(0, null, listener);
                }
            } catch (Exception e) {
                if (BuildConfig.DEBUG) {
                    e.printStackTrace();
                }
            }
            if (BuildConfig.DEBUG) {
                Log.d("MqttNet", "disConnectManually(),disconnect");
            }
            changeConnectState(PersistentConnectState.DISCONNECTED);
            PersistentEventDispatcher.getInstance().handlerMessage(PersistentEventDispatcher.MSG_CONNECT_STATE_DISCONNECTED, "disconnect success");
            try {
                if (memoryPersistence != null) {
                    memoryPersistence.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            memoryPersistence = null;
            mqttAsyncClient = null;
            inputStream = null;
            socketFactory = null;
            mqttParams = null;
        }
    }

    private SSLSocketFactory createSSLSocketFactory() throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, KeyManagementException {
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        TrustManager[] trustAllCerts = new TrustManager[1];
        TrustManager tm = new MqttTrustManager();
        trustAllCerts[0] = tm;
        sslContext.init(null, trustAllCerts, null);
        return sslContext.getSocketFactory();
    }

    private String getPassword(Map<String, String> params, String deviceSecret) {
        if (params != null && !TextUtils.isEmpty(deviceSecret)) {
            String[] keys = params.keySet().toArray(new String[0]);
            Arrays.sort(keys);
            StringBuilder sb = new StringBuilder();
            for (String key : keys) {
                if (!"sign".equalsIgnoreCase(key)) {
                    sb.append(key).append(params.get(key));
                }
            }
            try {
                SecretKeySpec hmacsha1 = new SecretKeySpec(deviceSecret.getBytes(StandardCharsets.UTF_8), "hmacsha1");
                Mac mac = Mac.getInstance(hmacsha1.getAlgorithm());
                mac.init(hmacsha1);
                byte[] bytes = mac.doFinal(sb.toString().getBytes(StandardCharsets.UTF_8));
                return toHexString(bytes);
            } catch (Exception e) {
                if (BuildConfig.DEBUG) {
                    Log.e("MqttNet", "hmacSign error, mqttConnectOptions" + e.toString());
                    e.printStackTrace();
                }
                return null;
            }
        } else {
            return null;
        }
    }

    private String toHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length);
        for (byte b : bytes) {
            String hexStr = Integer.toHexString(0xff & b);
            if (hexStr.length() < 2) {
                sb.append(0);
            }
            sb.append(hexStr.toUpperCase());
        }
        return sb.toString();
    }

    private static class InstanceHolder {
        private static final MqttNet INSTANCE = new MqttNet();
    }
}
